#define _CRT_SECURE_NO_WARNINGS 1


			《 Linux_17_地址空间 》


——————————————————————————————————————————————————————————————————————


——————————————————————————————————————————————————————————————————————


#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>

int g_val = 100;
int main()
{
	printf("father is running, pid: %d, ppid: %d\n", getpid(), getppid());
	pid_t id = fork();
	if (id == 0)
	{
		// child
		int cnt = 0;
		while (1)
		{
			printf("I am child  process, pid: %d, ppid: %d. g_val: %d, &g_val: %p\n", getpid(), getppid(), g_val, &g_val);
			sleep(1);
			cnt++;
			if (cnt == 5)
			{
				g_val = 300;
				printf("I am child  process, change %d -> %d\n", 100, 300);
			}
		}
	}
	else
	{
		//father
		printf("I am father process, pid: %d, ppid: %d. g_val: %d, &g_val: %p\n", getpid(), getppid(), g_val, &g_val);
		sleep(1);
	}

	return 0;
}

父子进程具有独立性
进程 = 内核数据结构(task_struct) + 代码(只读)和数据(读写)

father is running, pid: 23024，ppid : 9973
I am father process, pid : 23024，ppid : 9973.  g_val : 100， & g_val : 0x404654
I am child process，pid : 23025，ppid : 23024.  g_val : 100， & g_val : 0x404054
  ...
I am father process, pid:23024，ppid : 9973.    g_val : 100， & g_val : 0x404054
I am child process， pid : 23025，ppid : 23024. g_val : 100， & g_val : 0x404054
I am father process，pid : 23024，ppid : 9973.  g_val : 100， & g_val : 0x404054
I am child process,  change 100 -> 300
I am child process， pid : 23025，ppid : 23024. g_val : 300， & g_val : 0x404054   //子进程被改了，但是子进程和父进程还是同一个地址。
I am father process, pid : 23024，ppid : 9973.  g_val : 100， & g_val : 0x404054
I am child process， pid : 23025，ppid : 23024. g_val : 300， & g_val : 0x404054
I am father process，pid : 23024，ppid : 9973.  g_val : 100， & g_val : 0x404054

编译后：变量名和函数名在已经不存在了，全部替换成对应的地址了。
如果父子进程指向同一块空间，改变其中一个值，另一个值也应该会被改变，
而这里却出现了，一个地址同时是两个不同数据，这不合理。
由此我们可以推断出：这个地址不可能是真实存在的物理地址，真实的物理地址无法做到以上特性。
这是：【 虚拟地址 】

子进程会继承父进程的大部分数据，拷贝一份
页表也会被拷贝一份到子进程。虚拟地址与映射的物理地址都会被拷贝。
在修改子进程数据时，如果会影响到其他进程，会新建一块空间，并拷贝该数据，再进行修改数据。
页表由于是拷贝过来的，的虚拟地址一样，但实际指向的空间却不一样了。
这种有OS自主完成的方式我们叫做：【 写时拷贝 】


——————————————————————————————————————————————————————————————————————


——————————————————————————————————————————————————————————————————————


		《 虚拟地址空间 》

1.如何理解地址空间？
地址空间本质：是一个内核的 struct 结构体，内部很多属性都是表示 start, end 的范围。

 
2.为什么要有地址空间？
①.将无序变成有序，
让进程以统一的视角看待物理内存，以及自己运行的各个区域。方便管理。

②.进程管理模块和内存管理模块进行解耦。降低耦合

③.拦截非法请求，保护物理内存。


3.进一步理解 页表 和 写时拷贝
                  ①.数据是否在物理内存中。       不在：缺页中断，需要重新冲磁盘拷贝数据到内存，再从新访问
当 OS 识别到错误：②.是否需要 写时拷贝。          需要：发生写时拷贝。
                  ③.如果都不是，才进行异常处理。 拦截异常


4.如何理解虚拟地址？
虚拟地址空间 经过页表映射 物理地址空间，可以吧无序的物理内存地址，变得有序的管理起来。
由于中间存在一个缓冲的过程，这样可以更快捷的对，非法访问进行拦截。也便于内存管理。


		< 反汇编 > 
objdump -S test.c  // 对 test.c 进行反汇编
在最开始的时候，地址空间中页表里的数据从哪里来？
直接从程序当中读取。

程序里本身就有地址！！！
通过反汇编我们可以看到，这些都是:虚拟地址(逻辑地址)
每一次程序进行编译的时候会发现，它所对应的物理地址都会发生变化，是不一样的。
这种程序的编译方式称为：[ 平坦模式 ]


int main()
{
	//return XXX; return的本质就是对id进行写入
	//
	pid_t id = fork();	
发生写时拷贝时，虚拟地址是同一个地址，但物理地址是不同的，分别在自己所在的页表中，所以结果是两个不同的结果。
	
	if (id == 0)
	{
		// child
		while (1)
		{
			printf("I am child，%d，%p\n", id, &id);
			sleep(1);
		}
	}
	else if (id > 0)
	{
		while (1)
		{
			printf("I am father,%d，%p\n", id, &id);
			sleep(1);
		}
	}
	return 0;
}


——————————————————————————————————————————————————————————————————————


//Linux 真正的是如何调度的？
Linux 系统中，每一个 CPU 都有一个运行时队列： runqueue

queue[140]		//task_struct *queue[140] 
[   0    ]		//我们是以数组下标进行访问	
[   1    ]
[  ...   ]		//看起来有140个队列，实际只维护了 40 个优先级的队列(100~139)。
[   100  ]		// 60 + 40 = 100 最低优先级
[   139  ]		// 99 + 40 = 139 最高优先级
[   NULL ]

//		< bitmap 位图 > 
//那个位置为 1 表示该数据优先级所在位置！就不需要去遍历 queue 数组了。
bitmap[5]		//32 * 5 = 160 个比特位，只使用 140 个
[0000 0000][0000 0000][0000 0000][0000 0000]	// bitmap[0] == 0 ？一次就可以排查掉 32个比特位。时间复杂度 O(1)
[0000 0000][0000 0000][0000 0000][0000 0000]
[0000 0000][0000 0000][0000 0000][0000 0000]
[0000 0000][0000 0000][0000 0000][0000 0000]
[0000 0000][0000 0000][0000 0000][0000 0001]


过期队列：
过期队列和活动队列结构一模一样
过期队列上放置的进程，都是时间片耗尽的进程
当活动队列上的进程都被处理完毕之后，对过期队列的进程进行时间片重新计算

		< 大OE 调度算法 >
 使用双140队列，来进行轮转调度。
基于时间片，在进行轮转的时候，可以把不同的优先级 分别放在140队列的 100~139的下标当中，
相同优先级被放在通级别的子队列当中，当一个大队列的子进程调度完毕时，就把过期队列与运行时队列做交换(交换指针)。
需要轮转的放入下一个队列当中，等待下次调度。
这样即便优先度较低，也会在轮转的过程中得到较为均衡的调度。这就是我们的CPU


active指针和expired指针：
active指针永远指向活动队列expired指针永远指向过期队列
可是活动队列上的进程会越来越少，过期队列上的进程会越来越多，因为进程时间片到期时一直都存在的。
没关系，在合适的时候，只要能够交换active指针和expired指针的内容，就相当于有具有了一批新的活动进程!

总结：
在系统当中查找一个最合适调度的进程的时间复杂度是一个常数，不随着进程增多而导致时间成本增加，
我们称之为进程调度O(1)算法!

 

——————————————————————————————————————————————————————————————————————


——————————————————————————————————————————————————————————————————————
